package com.pomelo.base.core.config;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.stereotype.Component;

import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.filter.logging.Log4j2Filter;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
import com.github.pagehelper.PageInterceptor;
import com.pomelo.base.core.bean.DbTypeEnum;
import com.pomelo.base.core.datasource.DataSourceProperty;
import com.pomelo.base.core.datasource.DynamicDataSourceContextHolder;
import com.pomelo.base.core.util.DbUtil;
import com.pomelo.base.core.util.SpringUtil;

/**
 * 注册数据源
 *
 * @author biankudingcha
 * @date: 2021-07-23
 * @since 0.5.0
 */
@Component
@Order(1)
@ConditionalOnMissingBean(name = "CommonDataSourceApplicationRunner")
public class DataSourceApplicationRunner implements ApplicationRunner {

    /**
     * 日志对象
     */
    private static Log LOGGER = LogFactory.getLog(DataSourceApplicationRunner.class);

    /**
     * 数据源属性
     */
    @Autowired
    private DruidDataSourceProperties properties;

    /**
     * 数据库环境类型，默认生产环境
     */
    @Value("${datasource.environment:prod}")
    private String environment;

    /**
     * 会在服务启动完成后立即执行
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        try {
            String sql = "SELECT * FROM POMELO_SYS_DATASOURCE WHERE ENABLED_ = '1' AND ENVIRONMENT_ = ?";
            List<Map<String, Object>> datas = DbUtil.query(sql, new Object[] { environment });
            for (Map<String, Object> data : datas) {
                String dbId = (String) data.get("DATASOURCE_ID_");
                String driverClassName = (String) data.get("DATASOURCE_DRIVER_CLASS_NAME_");
                String url = (String) data.get("DATASOURCE_URL_");
                String username = (String) data.get("DATASOURCE_USERNAME_");
                String password = (String) data.get("DATASOURCE_PASSWORD_");
                String dbType = (String) data.get("DATASOURCE_DBTYPE_");
                String auto = (String) data.get("HIBERNATE_HBM2DDL_AUTO_");
                String schema = (String) data.get("HIBERNATE_DEFAULT_SCHEMA_");
                DataSourceProperty dataSourceProperty = new DataSourceProperty();
                dataSourceProperty.setDataSourceId(dbId);
                dataSourceProperty.setDbType(dbType);
                dataSourceProperty.setDriverClassName(driverClassName);
                dataSourceProperty.setUrl(url);
                dataSourceProperty.setUsername(username);
                dataSourceProperty.setPassword(password);
                dataSourceProperty.setHibernateHbm2ddlAuto(auto);
                dataSourceProperty.setHibernateDefaultSchema(schema);
                registerDataSource(dataSourceProperty);
                registerLocalSessionFactoryBean(dataSourceProperty);
                registerTransactionManager(dataSourceProperty);
                registerJdbcTemplate(dataSourceProperty);
                registerSqlSessionFactory(dataSourceProperty);
                registerSqlSessionTemplate(dataSourceProperty);
            }
            LOGGER.info("注册数据源完成");
        } catch (Exception e) {
            LOGGER.error("注册数据源失败", e);
        }
    }

    /**
     * 注册数据源
     *
     * @param dataSourceProperty 数据源属性
     * @throws Exception
     */
    public void registerDataSource(DataSourceProperty dataSourceProperty) throws Exception {
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringUtil
            .getApplicationContext();
        // 获取bean工厂并转换为DefaultListableBeanFactory
        DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext
            .getBeanFactory();
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(DruidDataSource.class);
        beanDefinitionBuilder.addPropertyValue("name", dataSourceProperty.getDataSourceId());
        beanDefinitionBuilder.addPropertyValue("dbType", dataSourceProperty.getDbType());
        beanDefinitionBuilder.addPropertyValue("driverClassName", dataSourceProperty.getDriverClassName());
        if (DbTypeEnum.H2.toString().equals(dataSourceProperty.getDbType()) && dataSourceProperty.getUrl() == null) {
            beanDefinitionBuilder.addPropertyValue("url", getH2DefaultUrl());
        } else {
            beanDefinitionBuilder.addPropertyValue("url", dataSourceProperty.getUrl());
        }
        beanDefinitionBuilder.addPropertyValue("username", dataSourceProperty.getUsername());
        beanDefinitionBuilder.addPropertyValue("password", dataSourceProperty.getPassword());
        beanDefinitionBuilder.addPropertyValue("initialSize", properties.getInitialSize());
        beanDefinitionBuilder.addPropertyValue("minIdle", 1);
        beanDefinitionBuilder.addPropertyValue("maxActive", properties.getMaxActive());
        beanDefinitionBuilder.addPropertyValue("maxWait", properties.getMaxWait());
        beanDefinitionBuilder.addPropertyValue("timeBetweenEvictionRunsMillis",
            properties.getTimeBetweenEvictionRunsMillis());
        beanDefinitionBuilder.addPropertyValue("minEvictableIdleTimeMillis",
            properties.getMinEvictableIdleTimeMillis());
        beanDefinitionBuilder.addPropertyValue("validationQuery", properties.getValidationQuery());
        beanDefinitionBuilder.addPropertyValue("testWhileIdle", properties.isTestWhileIdle());
        beanDefinitionBuilder.addPropertyValue("testOnBorrow", properties.isTestOnBorrow());
        beanDefinitionBuilder.addPropertyValue("testOnReturn", properties.isTestOnReturn());
        beanDefinitionBuilder.addPropertyValue("poolPreparedStatements", properties.isPoolPreparedStatements());
        beanDefinitionBuilder.addPropertyValue("maxPoolPreparedStatementPerConnectionSize",
            properties.getMaxPoolPreparedStatementPerConnectionSize());

        // 查询表的时候显示注释信息
        Properties connectionProperties = new Properties();
        connectionProperties.setProperty("remarks", "true");
        if (DbTypeEnum.MYSQL.toString().equalsIgnoreCase(dataSourceProperty.getDbType())) {
            connectionProperties.setProperty("useInformationSchema", "true");
        }
        beanDefinitionBuilder.addPropertyValue("connectProperties", connectionProperties);

        List<Filter> filters = new ArrayList<Filter>();
        Log4j2Filter log4j2Filter = getLog4j2Filter();
        filters.add(log4j2Filter);
        WallFilter wallFilter = getWallFilter(dataSourceProperty.getDbType());
        filters.add(wallFilter);
        beanDefinitionBuilder.addPropertyValue("proxyFilters", filters);
        String beanName = dataSourceProperty.getDataSourceId();
        defaultBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
        DynamicDataSourceContextHolder.getDataSourceIds().add(beanName);
        LOGGER.info("[" + beanName + "]数据源注册完成");
    }

    /**
     * 注册LocalSession hibernate
     *
     * @param dataSourceProperty 数据源属性
     * @throws Exception
     */
    public void registerLocalSessionFactoryBean(DataSourceProperty dataSourceProperty) throws Exception {
        String dataSourceId = dataSourceProperty.getDataSourceId();
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringUtil
            .getApplicationContext();
        // 获取bean工厂并转换为DefaultListableBeanFactory
        DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext
            .getBeanFactory();
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(LocalSessionFactoryBean.class);
        beanDefinitionBuilder.addPropertyReference("dataSource", dataSourceId);
        String dbType = dataSourceProperty.getDbType();
        String dialect = dataSourceProperty.getHibernateDialect();
        Properties hibernateProperties = new Properties();
        hibernateProperties.setProperty("hibernate.dialect", getHibernateDialect(dialect, dbType));
        if (DbTypeEnum.MYSQL.toString().equalsIgnoreCase(dbType)) {
            hibernateProperties.setProperty("hibernate.show_sql", "false");
        } else {
            hibernateProperties.setProperty("hibernate.show_sql", "true");
        }
        hibernateProperties.setProperty("hibernate.hbm2ddl.auto", "none");

        if (DbTypeEnum.ORACLE.toString().equalsIgnoreCase(dbType)) {
            hibernateProperties.setProperty("hibernate.default_schema", dataSourceProperty.getHibernateDefaultSchema());
        }
        beanDefinitionBuilder.addPropertyValue("hibernateProperties", hibernateProperties);
        String beanName = "SessionFactory-" + dataSourceId;
        defaultBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
        LOGGER.info("[" + beanName + "]SessionFactory注册完成");
        MultiDataSourceUtil.putLocalSessionFactory(beanName,
            SpringUtil.getBeanByName(LocalSessionFactoryBean.class, "&" + beanName));
    }

    /**
     * 注册事务
     *
     * @param dataSourceProperty 数据源属性
     * @throws Exception
     */
    public void registerTransactionManager(DataSourceProperty dataSourceProperty) throws Exception {
        String dataSourceId = dataSourceProperty.getDataSourceId();
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringUtil
            .getApplicationContext();
        // 获取bean工厂并转换为DefaultListableBeanFactory
        DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext
            .getBeanFactory();
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(HibernateTransactionManager.class);
        beanDefinitionBuilder.addConstructorArgReference("SessionFactory-" + dataSourceId);
        String beanName = "PlatformTransactionManager-" + dataSourceId;
        defaultBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
        LOGGER.info("[" + beanName + "]PlatformTransactionManager注册完成");
        MultiDataSourceUtil.putPlatformTransactionManager(beanName,
            SpringUtil.getBeanByName(HibernateTransactionManager.class, beanName));
    }

    /**
     * 注册JdbcTemplate spring
     *
     * @param dataSourceProperty 数据源属性
     * @throws Exception
     */
    public void registerJdbcTemplate(DataSourceProperty dataSourceProperty) throws Exception {
        String dataSourceId = dataSourceProperty.getDataSourceId();
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringUtil
            .getApplicationContext();
        // 获取bean工厂并转换为DefaultListableBeanFactory
        DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext
            .getBeanFactory();
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(JdbcTemplate.class);
        beanDefinitionBuilder.addConstructorArgReference(dataSourceId);
        String beanName = "JdbcTemplate-" + dataSourceId;
        defaultBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
        LOGGER.info("[" + beanName + "]JdbcTemplate注册完成");
    }

    /**
     * 注册SqlSessionFactory mybatis
     *
     * @param dataSourceProperty 数据源属性
     * @throws Exception
     */
    public void registerSqlSessionFactory(DataSourceProperty dataSourceProperty) throws Exception {
        String dataSourceId = dataSourceProperty.getDataSourceId();
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringUtil
            .getApplicationContext();
        // 获取bean工厂并转换为DefaultListableBeanFactory
        DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext
            .getBeanFactory();
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(SqlSessionFactoryBean.class);
        beanDefinitionBuilder.addPropertyReference("dataSource", dataSourceId);
        // 加载hibernate映射文件
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] mapperLocations = resolver.getResources("classpath*:/com/pomelo/**/mapping/*.xml");
        beanDefinitionBuilder.addPropertyValue("mapperLocations", mapperLocations);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setCallSettersOnNulls(true);
        configuration.setReturnInstanceForEmptyRow(true);
        beanDefinitionBuilder.addPropertyValue("configuration", configuration);
        // 分页插件
        Interceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        // 是否将参数offset作为PageNum使用
        properties.setProperty("offsetAsPageNum", "true");
        properties.setProperty("pageSizeZero", "true");
        interceptor.setProperties(properties);
        Interceptor[] interceptors = new Interceptor[] { interceptor };
        beanDefinitionBuilder.addPropertyValue("plugins", interceptors);
        String beanName = "SqlSessionFactory-" + dataSourceId;
        defaultBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
        LOGGER.info("[" + beanName + "]SqlSessionFactory注册完成");
    }

    /**
     * 注册SqlSession mybatis
     *
     * @param dataSourceProperty 数据源属性
     * @throws Exception
     */
    public void registerSqlSessionTemplate(DataSourceProperty dataSourceProperty) throws Exception {
        String dataSourceId = dataSourceProperty.getDataSourceId();
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringUtil
            .getApplicationContext();
        // 获取bean工厂并转换为DefaultListableBeanFactory
        DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext
            .getBeanFactory();
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(SqlSessionTemplate.class);
        beanDefinitionBuilder.addConstructorArgReference("SqlSessionFactory-" + dataSourceId);
        String beanName = "SqlSessionTemplate-" + dataSourceId;
        defaultBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
        LOGGER.info("[" + beanName + "]SqlSessionTemplate注册完成");
    }

    /**
     * 获取h2数据库默认url
     *
     * @return String
     */
    public String getH2DefaultUrl() {
        String rootPath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
        String url = "jdbc:h2:file:" + rootPath + "/h2;AUTO_SERVER=TRUE";
        return url;
    }

    /**
     * 获取druid的Log4j2Filter
     *
     * @return Log4j2Filter
     */
    public Log4j2Filter getLog4j2Filter() {
        Log4j2Filter log4j2Filter = new Log4j2Filter();
        log4j2Filter.setConnectionLogEnabled(true);
        log4j2Filter.setStatementLogEnabled(true);
        log4j2Filter.setResultSetLogEnabled(false);
        log4j2Filter.setStatementExecutableSqlLogEnable(true);
        return log4j2Filter;
    }

    /**
     * 获取WallFilter
     *
     * @param dbType 数据库类型
     * @return WallFilter
     */
    public WallFilter getWallFilter(String dbType) {
        WallConfig wallConfig = new WallConfig();
        if (DbTypeEnum.ORACLE.toString().equalsIgnoreCase(dbType)) {
            wallConfig.setDir("META-INF/druid/wall/oracle");
        } else if (DbTypeEnum.MYSQL.toString().equalsIgnoreCase(dbType)) {
            wallConfig.setDir("META-INF/druid/wall/mysql");
        }
        wallConfig.setMultiStatementAllow(true);
        // 检查SELECT语句的WHERE子句是否是一个永真条件 false为不校验
        wallConfig.setSelectWhereAlwayTrueCheck(false);
        // 检查查询条件(WHERE/HAVING子句)中是否包含AND永真条件 true为允许
        wallConfig.setConditionAndAlwayTrueAllow(true);
        // 检查查询条件(WHERE/HAVING子句)中是否包含AND永假条件 true为允许
        wallConfig.setConditionAndAlwayFalseAllow(true);
        WallFilter wallFilter = new WallFilter();
        wallFilter.setConfig(wallConfig);
        return wallFilter;
    }

    /**
     * 获取HibernateDialect 
     *
     * @param dialect dialect
     * @param dbType 数据库类型
     * @return String
     */
    public String getHibernateDialect(String dialect, String dbType) {
        if (dialect != null && !"".equals(dialect)) {
            return dialect;
        } else if (DbTypeEnum.ORACLE.toString().equalsIgnoreCase(dbType)) {
            return "org.hibernate.dialect.Oracle10gDialect";
        } else if (DbTypeEnum.MYSQL.toString().equalsIgnoreCase(dbType)) {
            return "org.hibernate.dialect.MySQL5InnoDBDialect";
        } else if (DbTypeEnum.H2.toString().equalsIgnoreCase(dbType)) {
            return "org.hibernate.dialect.H2Dialect";
        } else {
            return dialect;
        }
    }

}